Complete JavaScript Hoisting Guide
Table of Contentsโ
- What Hoisting Actually Is
- The Two-Phase Mechanism
- Hoisting Rules Reference Table
- Temporal Dead Zone (TDZ)
- var Hoisting
- let and const Hoisting
- Function Declaration Hoisting
- Function Expression Hoisting
- Arrow Function Hoisting
- Class Hoisting
- Interview Problems (Beginner to Advanced)
- Hoisting with Closures
- Hoisting with Async Code
- Block Scope Edge Cases
- Advanced Collision Scenarios
- typeof Operator Behavior
- Common Production Bugs
- Mental Model for Interviews
- Interview Checklist
- Ultra-Hard Puzzles
What Hoisting Actually Isโ
Hoisting โ moving code to the top
This is the most common misconception. JavaScript doesn't physically move your code. Instead, it processes your code in two distinct phases:
The Truth About Hoistingโ
During the creation phase, the JavaScript engine scans through your code and:
- Registers all variable and function declarations
- Allocates memory for them
- Initializes them according to specific rules (depends on declaration type)
During the execution phase, the code runs line by line with those pre-registered bindings already in place.
The Two-Phase Mechanismโ
Phase 1: Creation (Memory Allocation)โ
// What you write:
console.log(x);
var x = 5;
// How JS processes it:
// CREATION PHASE:
var x = undefined; // Memory allocated, initialized to undefined
// EXECUTION PHASE:
console.log(x); // undefined
x = 5; // Assignment happens
Creation phase behavior by type:
| Declaration Type | Created | Initialized | Value |
|---|---|---|---|
var | โ | โ | undefined |
let | โ | โ | uninitialized (TDZ) |
const | โ | โ | uninitialized (TDZ) |
| Function declaration | โ | โ | Entire function |
| Function expression | Variable only | โ | undefined (if var) |
| Arrow function | Variable only | โ | undefined (if var) |
| Class | โ | โ | uninitialized (TDZ) |
Phase 2: Executionโ
Code runs line by line, assignments happen, functions are called.
Hoisting Rules Reference Tableโ
| Construct | Hoisted | Initialized | Access before declaration | Scope |
|---|---|---|---|---|
var | โ | undefined | โ
(returns undefined) | Function |
let | โ | โ | โ (ReferenceError - TDZ) | Block |
const | โ | โ | โ (ReferenceError - TDZ) | Block |
| Function declaration | โ | โ | โ (fully usable) | Function/Block* |
| Function expression | Variable only | โ | โ | Depends on var/let/const |
| Arrow function | Variable only | โ | โ | Depends on var/let/const |
| Class | โ | โ | โ (ReferenceError - TDZ) | Block |
*Function declarations in blocks are complex and mode-dependent
Temporal Dead Zone (TDZ)โ
The TDZ is the time between entering a scope and the actual declaration line where a variable exists but cannot be accessed.
Basic TDZ Exampleโ
console.log(a); // โ ReferenceError: Cannot access 'a' before initialization
let a = 10;
The variable a exists in memory but is in the TDZ until line 2 executes.
TDZ in Block Scopeโ
{
// TDZ starts here for 'x'
console.log(x); // โ ReferenceError
let x = 1; // TDZ ends here
}
TDZ with Functionsโ
function test() {
// TDZ starts for 'data'
console.log(data); // โ ReferenceError
let data = 100; // TDZ ends
}
test();
TDZ is Temporal, Not Spatialโ
function useX() {
console.log(x); // โ ReferenceError
}
let x = 10;
useX(); // This call itself is fine, but the function body accesses x before declaration
Wait, this actually works because by the time useX() is called, x is already declared. The TDZ only exists during the execution of the scope where the variable is declared.
Correct example:
let x = 10;
function useX() {
console.log(x); // โ
10 - works fine
}
useX();
TDZ matters when accessing before declaration in the same scope:
function useX() {
console.log(x); // โ ReferenceError - TDZ
let x = 10; // Declaration in same scope
}
useX();
typeof in TDZโ
console.log(typeof a); // โ ReferenceError
let a = 10;
But with undeclared variables:
console.log(typeof undeclaredVariable); // โ
"undefined"
๐ Key insight: typeof does NOT bypass TDZ for declared variables.
var Hoistingโ
var declarations are hoisted and initialized to undefined.
Basic var Hoistingโ
console.log(x); // undefined
var x = 5;
console.log(x); // 5
Behind the scenes:
var x = undefined; // Creation phase
console.log(x); // undefined
x = 5; // Execution phase
console.log(x); // 5
var in Functionsโ
function test() {
console.log(a); // undefined
var a = 10;
console.log(a); // 10
}
test();
var Scope (Function-scoped)โ
function example() {
if (true) {
var x = 10;
}
console.log(x); // โ
10 - var ignores block scope
}
example();
Multiple var Declarationsโ
var a = 1;
var a = 2;
console.log(a); // 2 - allowed, no error
var in Global Scopeโ
var globalVar = 'test';
console.log(window.globalVar); // 'test' (in browsers)
let and const Hoistingโ
Both let and const are hoisted but not initialized, creating a TDZ.
let Hoistingโ
console.log(x); // โ ReferenceError
let x = 10;
const Hoistingโ
console.log(y); // โ ReferenceError
const y = 20;
Block Scopeโ
{
let x = 10;
const y = 20;
}
console.log(x); // โ ReferenceError - not defined outside block
console.log(y); // โ ReferenceError
No Re-declarationโ
let a = 1;
let a = 2; // โ SyntaxError: Identifier 'a' has already been declared
const b = 1;
const b = 2; // โ SyntaxError
const Requires Initializationโ
const x; // โ SyntaxError: Missing initializer in const declaration
Function Declaration Hoistingโ
Function declarations are fully hoisted and initialized.
Basic Function Hoistingโ
sayHello(); // โ
"Hello!"
function sayHello() {
console.log("Hello!");
}
Behind the scenes:
// Creation phase:
function sayHello() {
console.log("Hello!");
}
// Execution phase:
sayHello(); // โ
"Hello!"
Function Overridingโ
foo(); // "second"
function foo() {
console.log("first");
}
function foo() {
console.log("second");
}
Last declaration wins during creation phase.
Function Expression Hoistingโ
Function expressions only hoist the variable, not the function.
var Function Expressionโ
sayHi(); // โ TypeError: sayHi is not a function
var sayHi = function() {
console.log("Hi!");
};
Why TypeError, not ReferenceError?
// Behind the scenes:
var sayHi = undefined; // Hoisted
sayHi(); // undefined is not a function
sayHi = function() { // Assignment happens after
console.log("Hi!");
};
let/const Function Expressionโ
greet(); // โ ReferenceError: Cannot access 'greet' before initialization
const greet = function() {
console.log("Greetings!");
};
Arrow Function Hoistingโ
Arrow functions behave like function expressions.
const Arrow Functionโ
sayHi(); // โ ReferenceError
const sayHi = () => {
console.log("Hi!");
};
var Arrow Functionโ
sayHi(); // โ TypeError: sayHi is not a function
var sayHi = () => {
console.log("Hi!");
};
Class Hoistingโ
Classes are hoisted but remain in TDZ until declaration.
Basic Class Hoistingโ
const obj = new Person(); // โ ReferenceError
class Person {
constructor(name) {
this.name = name;
}
}
Class Expressionโ
const obj = new MyClass(); // โ ReferenceError
const MyClass = class {
constructor() {}
};
Interview Problemsโ
๐ฅ Problem 1: var in Function Scopeโ
function test() {
console.log(a);
var a = 10;
}
test();
Output: undefined
Explanation: var a is hoisted to the top of the function and initialized to undefined.
๐ฅ Problem 2: let vs varโ
console.log(a);
var a = 1;
console.log(b);
let b = 2;
Output:
undefined
ReferenceError: Cannot access 'b' before initialization
๐ฅ Problem 3: Function Declaration vs varโ
foo();
function foo() {
console.log("function");
}
var foo = function() {
console.log("var");
};
Output: "function"
Explanation:
// Creation phase:
function foo() { console.log("function"); } // Function fully hoisted
var foo = undefined; // var is also hoisted but doesn't override function
// Execution phase:
foo(); // Calls the function
var foo = function() { console.log("var"); }; // Reassignment happens after
๐ฅ Problem 4: Function in Block (Strict vs Non-Strict)โ
"use strict";
{
function foo() {
return "inside block";
}
}
foo(); // โ ReferenceError in strict mode
Non-strict mode (browser-dependent):
{
function foo() {
return "inside block";
}
}
foo(); // May work in some browsers
๐ Best practice: Never use function declarations inside blocks. Use function expressions instead.
๐ฅ Problem 5: Arrow Function Hoistingโ
sayHi();
const sayHi = () => {
console.log("hi");
};
Output: ReferenceError: Cannot access 'sayHi' before initialization
๐ฅ Problem 6: Class Hoistingโ
const obj = new Person();
class Person {
constructor(name) {
this.name = name;
}
}
Output: ReferenceError: Cannot access 'Person' before initialization
๐ฅ Problem 7: Shadowingโ
var a = 1;
function test() {
console.log(a);
var a = 2;
}
test();
Output: undefined
Explanation: Local var a shadows the outer a and is hoisted to the top of the function.
๐ฅ Problem 8: Function Parametersโ
function foo(a) {
console.log(a);
var a = 20;
}
foo(10);
Output: 10
Explanation: Parameter a = 10 is already initialized. The var a = 20 declaration is ignored (parameter takes precedence), but the assignment a = 20 would happen after the console.log.
๐ฅ Problem 9: Duplicate Declarationsโ
var a = 1;
var a = 2;
console.log(a); // 2 - allowed
let b = 1;
let b = 2; // โ SyntaxError: Identifier 'b' has already been declared
๐ฅ Problem 10: Loop Hoistingโ
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
Output:
3
3
3
Explanation: var i is function-scoped (or global if not in a function), so there's only one binding shared across all iterations.
Fix with let:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Output: 0, 1, 2
let creates a new binding for each iteration.
Hoisting with Closuresโ
Problem 11: var in Closureโ
function createFunctions() {
var result = [];
for (var i = 0; i < 3; i++) {
result.push(function() {
return i;
});
}
return result;
}
const funcs = createFunctions();
console.log(funcs[0]()); // 3
console.log(funcs[1]()); // 3
console.log(funcs[2]()); // 3
Explanation: All closures reference the same i variable.
Fix:
function createFunctions() {
var result = [];
for (let i = 0; i < 3; i++) {
result.push(function() {
return i;
});
}
return result;
}
const funcs = createFunctions();
console.log(funcs[0]()); // 0
console.log(funcs[1]()); // 1
console.log(funcs[2]()); // 2
Problem 12: Closure with setTimeoutโ
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
Output: 4, 4, 4 (after 1s, 2s, 3s)
Fix with IIFE:
for (var i = 1; i <= 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, j * 1000);
})(i);
}
// Output: 1, 2, 3
Fix with let:
for (let i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
// Output: 1, 2, 3
Hoisting with Async Codeโ
Problem 13: Hoisting in Async Functionsโ
async function test() {
console.log(x); // undefined
var x = 10;
console.log(x); // 10
}
test();
Explanation: Hoisting works the same in async functions.
Problem 14: Await and Hoistingโ
async function getData() {
console.log(data); // โ ReferenceError
const data = await fetch('/api');
}
Explanation: const is in TDZ before declaration, even with await.
Problem 15: Promise and varโ
for (var i = 0; i < 3; i++) {
Promise.resolve().then(() => console.log(i));
}
Output: 3, 3, 3
Fix:
for (let i = 0; i < 3; i++) {
Promise.resolve().then(() => console.log(i));
}
// Output: 0, 1, 2
Block Scope Edge Casesโ
Problem 16: Nested Blocksโ
{
console.log(x); // โ ReferenceError
{
let x = 10;
}
}
Problem 17: if Blockโ
if (true) {
var x = 10;
}
console.log(x); // โ
10
if (true) {
let y = 20;
}
console.log(y); // โ ReferenceError
Problem 18: Switch Caseโ
switch (1) {
case 1:
let x = 10;
console.log(x); // 10
break;
case 2:
let x = 20; // โ SyntaxError: Identifier 'x' has already been declared
break;
}
Explanation: The entire switch block is one scope.
Fix:
switch (1) {
case 1: {
let x = 10;
console.log(x);
break;
}
case 2: {
let x = 20;
console.log(x);
break;
}
}
Problem 19: for Loop Scopeโ
for (let i = 0; i < 3; i++) {
let i = 'inner'; // Different variable
console.log(i); // 'inner', 'inner', 'inner'
}
Advanced Collision Scenariosโ
Problem 20: Function vs var Collisionโ
console.log(foo); // [Function: foo]
var foo = "variable";
function foo() {
return "function";
}
console.log(foo); // "variable"
Explanation:
// Creation phase:
function foo() { return "function"; } // Function hoisted
var foo; // var declaration (but doesn't override the function)
// Execution phase:
console.log(foo); // [Function: foo]
foo = "variable"; // Assignment happens
console.log(foo); // "variable"
Problem 21: Multiple Functions Same Nameโ
foo(); // "third"
function foo() {
console.log("first");
}
function foo() {
console.log("second");
}
function foo() {
console.log("third");
}
Explanation: Last function declaration wins.
Problem 22: Parameter vs varโ
function test(x) {
console.log(x); // 10
var x;
console.log(x); // 10
x = 20;
console.log(x); // 20
}
test(10);
Explanation: Parameter declaration takes precedence. var x; is redundant.
Problem 23: let in Parameter Defaultโ
function test(a = b, b = 2) {
console.log(a, b);
}
test(); // โ ReferenceError: Cannot access 'b' before initialization
Explanation: Parameters are evaluated left to right. b is in TDZ when a's default is evaluated.
Fix:
function test(b = 2, a = b) {
console.log(a, b);
}
test(); // 2, 2
typeof Operator Behaviorโ
Problem 24: typeof with Undeclared Variableโ
console.log(typeof undeclaredVariable); // "undefined"
Problem 25: typeof with TDZโ
console.log(typeof x); // โ ReferenceError
let x = 10;
Problem 26: typeof with varโ
console.log(typeof y); // "undefined"
var y = 10;
Common Production Bugsโ
Bug 1: Conditional var Declarationโ
โ Buggy:
function checkStatus() {
if (!isReady) {
var isReady = true;
}
return isReady;
}
console.log(checkStatus()); // undefined (not true!)
Behind the scenes:
function checkStatus() {
var isReady; // Hoisted to top, initialized as undefined
if (!isReady) { // undefined is falsy
isReady = true;
}
return isReady;
}
โ Fix:
function checkStatus() {
let isReady = false;
if (!isReady) {
isReady = true;
}
return isReady;
}
console.log(checkStatus()); // true
Bug 2: Loop Event Handlersโ
โ Buggy:
for (var i = 0; i < buttons.length; i++) {
buttons[i].onclick = function() {
alert(i); // Always alerts buttons.length
};
}
โ Fix:
for (let i = 0; i < buttons.length; i++) {
buttons[i].onclick = function() {
alert(i); // Alerts correct index
};
}
Bug 3: Async var Mutationโ
โ Buggy:
for (var i = 0; i < 5; i++) {
fetch('/api/' + i).then(response => {
console.log('Response for', i); // Always 5
});
}
โ Fix:
for (let i = 0; i < 5; i++) {
fetch('/api/' + i).then(response => {
console.log('Response for', i); // Correct index
});
}
Mental Model for Interviewsโ
When explaining hoisting in interviews, use this mental model:
"JavaScript processes code in two phases. During the creation phase, it scans through the scope and registers all variable and function declarations, allocating memory and initializing them according to specific rulesโ
vargetsundefined, functions get their full definition, andlet/const/classes enter a temporal dead zone where they exist but can't be accessed. Then during the execution phase, the code runs line by line with those pre-registered bindings already in place. This is why you can call a function before it's declared, but accessing aletvariable before declaration throws a ReferenceError."
Key phrases to use:
- "Two-phase mechanism: creation and execution"
- "Memory allocation vs initialization"
- "Temporal Dead Zone for let/const"
- "Scope registration before execution"
- "Function declarations are fully initialized"
Interview Checklistโ
โ
Function declarations โ Fully hoisted and initialized
โ
var โ Hoisted, initialized to undefined
โ
let/const/class โ Hoisted but in TDZ, uninitialized
โ
Arrow functions โ Not hoisted as functions
โ
Function expressions โ Variable hoisted (if var), function not
โ
Parameters โ Take precedence over var in same scope
โ
Closures + var โ Single shared binding (common bug)
โ
Block scope โ let/const respect blocks, var doesn't
โ
typeof โ Doesn't bypass TDZ
โ
Switch cases โ Share one block scope
Ultra-Hard Puzzlesโ
๐ฅ Puzzle 1: Complex Shadowingโ
var a = 1;
function outer() {
console.log(a);
function inner() {
console.log(a);
var a = 3;
}
inner();
console.log(a);
var a = 2;
}
outer();
console.log(a);
Output:
undefined
undefined
undefined
1
Explanation:
- First
console.log(a)inouter: localvar ais hoisted โundefined - In
inner,var ais hoisted โundefined - After
inner(), stillundefinedinouter - After
outer(), globalais still1
๐ฅ Puzzle 2: Function Expression Collisionโ
var foo = 1;
function bar() {
console.log(foo);
foo = 10;
function foo() {}
console.log(foo);
}
bar();
console.log(foo);
Output:
[Function: foo]
10
1
Explanation:
- Function
foois hoisted inbar's scope foo = 10reassigns the local function- Global
fooremains1
๐ฅ Puzzle 3: TDZ with Function Callโ
let x = 1;
function test() {
console.log(x);
let x = 2;
}
test();
Output: ReferenceError: Cannot access 'x' before initialization
Explanation: Local let x creates TDZ in test, shadowing outer x.
๐ฅ Puzzle 4: Multiple Scopesโ
var x = 1;
{
var x = 2;
{
let x = 3;
console.log(x);
}
console.log(x);
}
console.log(x);
Output:
3
2
2
Explanation: var ignores block scope, let respects it.
๐ฅ Puzzle 5: Async + Hoistingโ
for (var i = 0; i < 3; i++) {
setTimeout(() => {
var j = i;
console.log(j);
}, 100);
}
Output: 3, 3, 3 (after 100ms)
Explanation: var i is shared. Each timeout captures the final value of i.
๐ฅ Puzzle 6: Class Expressionโ
const MyClass = class InternalName {
constructor() {
console.log(InternalName);
}
};
new MyClass(); // [class InternalName]
console.log(InternalName); // โ ReferenceError
Explanation: Class expression name is only visible inside the class.
๐ฅ Puzzle 7: Destructuring Hoistingโ
console.log(a); // โ ReferenceError
let { a } = { a: 1 };
console.log(b); // undefined
var { b } = { b: 2 };
Explanation: Destructuring follows the same hoisting rules as regular declarations.
๐ฅ Puzzle 8: Function Parameter Default with Closureโ
var x = 1;
function foo(x = x) {
console.log(x);
}
foo(); // โ ReferenceError: Cannot access 'x' before initialization
Explanation: Parameter x is in TDZ when its own default value is evaluated.
๐ฅ Puzzle 9: Nested Function Hoistingโ
function outer() {
inner(); // โ
Works
function inner() {
console.log("inner");
}
}
outer();
function outer() {
inner(); // โ ReferenceError
const inner = function() {
console.log("inner");
};
}
outer();
๐ฅ Puzzle 10: Mixed Declarationsโ
console.log(typeof foo); // "function"
console.log(typeof bar); // "undefined"
var bar = function() {};
function foo() {}
Explanation: Function declaration hoisted completely, function expression only hoists the variable.
๐ฅ Puzzle 11: with Statement (Don't use in production!)โ
var obj = { a: 1 };
with (obj) {
console.log(a); // 1
var a = 2; // Creates global variable!
}
console.log(a); // 2
console.log(obj.a); // 1
Explanation: var inside with doesn't create a property on obj, it creates a global variable.
๐ฅ Puzzle 12: eval and Hoisting (Avoid eval!)โ
function test() {
eval("var x = 10");
console.log(x); // 10 (in non-strict mode)
}
test();
In strict mode:
"use strict";
function test() {
eval("var x = 10");
console.log(x); // โ ReferenceError
}
test();
๐ฅ Puzzle 13: Generator Functionโ
gen(); // โ ReferenceError
const gen = function* () {
yield 1;
};
gen(); // โ
Works
function* gen() {
yield 1;
}
Explanation: Generator expressions follow function expression rules, generator declarations follow function declaration rules.
๐ฅ Puzzle 14: Arrow Function with varโ
test(); // โ TypeError: test is not a function
var test = () => console.log("arrow");
๐ฅ Puzzle 15: Import Hoistingโ
// This works:
myFunction();
import { myFunction } from './module.js';
Explanation: Imports are hoisted to the top of the module and initialized before any code runs.